【レポート】『FINAL FANTASY VII EVER CRISIS』全世界同時リリースを支えた大規模負荷に耐えるハイパフォーマンスなゲームサーバーの仕組み #CEDEC2024 #classmethod_game
こんにちは、ゲームソリューション部の入井です。
今回はCEDEC2024で聴講したセッション『『FINAL FANTASY VII EVER CRISIS』全世界同時リリースを支えた大規模負荷に耐えるハイパフォーマンスなゲームサーバーの仕組み』についてレポートします。
セッション概要
本セッションでは、『FINAL FANTASY VII EVER CRISIS』のゲームサーバーが「全世界同時リリース」と「250万DAUに対応する構成」の目標を達成するために採用したパフォーマンス向上策と、実際に20万rpsを計測した負荷試験とチューニングの内容を紹介します。具体的には、リクエスト単位でキャッシュする独自開発のORMを用いたデータベースアクセス数の削減や、共通のリクエスト・レスポンスを活用したユーザーデータの差分同期など、効率化へのさまざまなアプローチにより、1vCPUあたり300rpsを処理できるGo言語で実装したゲームサーバーの構造を詳しく述べます。また、全世界同時リリースを実現するための多言語対応やマスター管理の方法についても解説します。
※CEDEC2024セッションページより引用
印象に残った点
FINAL FANTASY VII EVER CRISISのサーバー要件
FF7ECはスマートフォンとPCで全世界向けに展開するタイトルということで、サーバー側の要件としては250万DAUという超大規模なアクセスに耐えられることを求められました。そこで、サーバーインフラを構築するにあたり以下の目標を立てたとのことです。
- 負荷試験で20万rps(Request Per Second)を達成
- 全世界同時リリースのための多言語対応
- マルチプレイに使用するリアルタイム通信用サーバーは50万CCU(同時アクセス)に耐えること
インフラは基本的にAWS上に作られており、クライアントからのAPIリクエストを処理するゲームサーバーはCloudFront+ALB+ECSとストレージとしてS3, Aurora, ElastiCacheで構築、リアルタイムマルチプレイに使用するバトルサーバーはAmazon GameLiftが使用されています。その他のデータ分析基盤やアセット配信にはAWS以外のものが使われているとのことです。
また、FF7 ECにおけるGameLiftの利用については、AWS公式で事例としても紹介されています。
ContextCachedORMによるDBアクセスの削減
サーバーのパフォーマンス向上の取り組みとして最も重要度が高かったのが、DBアクセスの削減でした。それを実現するために、ContextCachedORMというGo言語ライブラリを自社で開発したとのことです。
ContextCachedORMは『ゲームで必要なことに特化したORM』で、DBアクセスを減らすための様々なキャッシュ機能を持っています。ゲームに特化してパフォーマンスを最適化する関係上JOINには未対応等の制約がありますが、通常は制約に引っかかるような使い方をすることはあまりないとのことです。
読み込みキャッシュ機能には、何らかのクエリによってDBから取得したデータを保存するテーブルキャッシュと、クエリの検索条件を保存するクエリキャッシュがあります。
テーブルキャッシュはキャッシュデータを返すときに使用され、クエリキャッシュは取得しようとしているデータがキャッシュに存在しないだけなのかDBにも存在しないのかを判断するために使われます。
クエリキャッシュについては、検索条件がキャッシュ済のクエリと完全一致しなければいけないわけではなく、過去に発行した検索条件が複数あるクエリと部分一致をした場合であってもキャッシュ済と判定されるようになっています。これにより、検索済のデータに対するアクセスが一切発生しないようにできたとのことです。
書き込みキャッシュ機能では、データの追加・更新・削除といった処理を、読み込みキャッシュで作成したテーブルキャッシュに対して実行することで、DBアクセス数を削減しています。また、後でテーブルキャッシュのデータをDBに適切に反映できるように、そのデータがどういう状態であるのかという情報もテーブルキャッシュの各レコードに書き込んでいます。
キャッシュの状態は以下の4つがあります。
- SELECT
- INSERT
- UPDATE
- DELETE
この状態にもとづいて、キャッシュへの書き込み処理の内容が変わります。例えば、あるデータを追加しようとして、そのデータが既にテーブルキャッシュ上にSELECT、UPDATE、INSERTなどの状態で存在する場合は処理に矛盾が生じるためエラーが返り、一方でデータが存在しなければINSERT状態でデータを追加、存在していてもDELETEであればUPDATEとして更新するというような形です。
キャッシュテーブルにたまったデータは、バルク処理によって一括でDBに書き込まれます。書き込みクエリで何が使われるかは、テーブルキャッシュの状態によって決まるようになっています。
ContextCachedORMの活用例として、まずログイン時のユーザーデータの差分同期が紹介されました。ログインの際にクライアントへ返すユーザーデータの更新差分をキャッシュから返すことで、DBアクセスの削減やデータ整合性の向上を実現しています。
また、同一テーブルの複数データを取得する処理で、あらかじめIN句のクエリで必要なデータを一括でキャッシュに事前ロードしておくことで、個別のクエリによるDBアクセスを発生させないという活用法も紹介されました。
負荷試験によるチューニング
ゲームサーバーへの負荷試験では、1時間程度の瞬間的なアクセス増(スパイク)のための最高値として20万rps(Request Per Second)、定常時は5万rpsを達成することを目標として設定しました。シナリオは通常プレイの場合と、特殊なAPIアクセス手順となるリセマラを想定した場合のものを用意しています。
負荷試験は、まずAPI1台に対して負荷をかけた結果を確認するミニマム試験から開始しました。
試験の実施→ボトルネックの調査→ボトルネックの対応というサイクルを回すことで、1つずつ確実にボトルネックを潰していきます。ボトルネック対応の具体的な内容としては、各種ライブラリ・Goの設定変更・バージョンアップや、EC2インスタンスタイプ変更等のインフラ調整、トランザクション時間短縮等のAPIロジックの調整などを行ったとのことです。
各種ボトルネックの対応を行うことで、1vcpuあたりのパフォーマンスが90rpsから380rpsまで向上したとのことです。
感想
普段自分が業務で関わっている領域のセッションということで、終始興味深く聴講することができました。
最近の運営型ゲームは、このセッションで紹介されたFF7ECのようにスマートフォン+PC(更にタイトルによってはコンシューマ)といった複数プラットフォームかつ全世界規模でのリリースを行うケースが増えています。より多くのプレイヤーにリーチをするためにこのようなリリース戦略を取っていると思われますが、その分サーバーインフラ側は従来よりも更に大規模なアクセスに耐えられるよう備えておく必要があります。
インフラの負荷試験やチューニングは、運営型ゲームのインフラ構築にあたって今後ますます重要になってくると思うので、今回のセッションで紹介された負荷削減方法等はしっかりと頭に入れておき、今後の自身の業務に役立てていきたいと思いました。